﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Reflection;

namespace Wicresoft.EKA.NRemedy.Linq
{
    public class QueryVisitor : System.Linq.Expressions.ExpressionVisitor
    {
        private TranslateResult tr;

        public QueryVisitor()
        {
            tr = new TranslateResult();
        }

        internal TranslateResult Translate(Expression expression)
        {
            this.Visit(expression);
            return tr;
        }

        private static Expression StripQuotes(Expression e)
        {
            while (e.NodeType == ExpressionType.Quote)
            {
                e = ((UnaryExpression)e).Operand;
            }
            return e;
        }

        protected override Expression VisitMethodCall(MethodCallExpression m)
        {

            if (m.Method.DeclaringType == typeof(Queryable))
            {
                switch (m.Method.Name)
                {
                    case "Where":
                        this.Visit(m.Arguments[0]);
                        LambdaExpression lambdaWhere = (LambdaExpression)StripQuotes(m.Arguments[1]);
                        tr.CurrentOpeation = TranslateResult.Operation.where;
                        this.Visit(lambdaWhere.Body);
                        break;
                    case "Select":
                        this.Visit(m.Arguments[0]);
                        LambdaExpression lambdaSelect = (LambdaExpression)StripQuotes(m.Arguments[1]);
                        tr.SelectExpression = lambdaSelect;
                        tr.CurrentOpeation = TranslateResult.Operation.select;
                        this.Visit(lambdaSelect.Body);
                        break;
                    case "OrderBy":
                    case "ThenBy":
                        this.Visit(m.Arguments[0]);
                        LambdaExpression lambdaOrderBy = (LambdaExpression)StripQuotes(m.Arguments[1]);
                        tr.CurrentOpeation = TranslateResult.Operation.orderby;
                        this.Visit(lambdaOrderBy.Body);
                        break;
                    case "OrderByDescending":
                    case "ThenByDescending":
                        this.Visit(m.Arguments[0]);
                        LambdaExpression lambdaOrderByDescending = (LambdaExpression)StripQuotes(m.Arguments[1]);
                        tr.CurrentOpeation = TranslateResult.Operation.orderbydescending;
                        this.Visit(lambdaOrderByDescending.Body);
                        break;
                    case "Take":
                        this.Visit(m.Arguments[0]);
                        ConstantExpression constant = m.Arguments[1] as ConstantExpression;
                        if (constant == null)
                        {
                            throw new NotImplementedException("Take supported only for constant values");
                        }
                        tr.Take = (int)constant.Value;
                        break;
                    case "Skip":
                        this.Visit(m.Arguments[0]);
                        ConstantExpression constantSkip = m.Arguments[1] as ConstantExpression;
                        if (constantSkip == null)
                        {
                            throw new NotImplementedException("Take supported only for constant values");
                        }
                        tr.Skip = (int)constantSkip.Value;
                        break;
                    case "Count":
                        this.Visit(m.Arguments[0]);
                        if (m.Arguments.Count == 2)
                            throw new NotSupportedException("Count clause not support parameter,use where clause instead.");
                        tr.Count = true;
                        break;
                    case "GroupBy":
                        this.Visit(m.Arguments[0]);
                        if (m.Arguments.Count > 2)
                            throw new NotSupportedException("Count clause not support parameter,use where clause instead.");
                        LambdaExpression GroupBy = (LambdaExpression)StripQuotes(m.Arguments[1]);
                        tr.CurrentOpeation = TranslateResult.Operation.groupby;
                        this.Visit(GroupBy.Body);
                        break;
                    default:
                        throw new NotSupportedException(string.Format("The method '{0}' is not supported", m.Method.Name));

                }
            }
            else if (m.Method.DeclaringType == typeof(string))
            {
                // FIRST STEP
                // We support only a comparison between constant 
                // and a possible flight query parameter 
                ConstantExpression constant =
                    (m.Object as ConstantExpression ?? m.Arguments[0] as ConstantExpression);
                MemberExpression memberAccess =
                    (m.Object as MemberExpression ?? m.Arguments[0] as MemberExpression);

                // SECOND STEP
                // Sanity check of parameters
                if ((memberAccess == null) || (constant == null))
                {
                    throw new NotSupportedException(
                        string.Format(
                            "The binary operator about string '{0}' must compare a valid flight attribute with a constant",
                            m.NodeType));
                }

                // We need to get the constant value
                if (constant.Value == null)
                {
                    throw new NotSupportedException(
                        string.Format(
                            "NULL constant is not supported in binary operator about string {0}",
                            m.ToString()));
                }

                switch (m.Method.Name)
                {
                    case "Contains":
                        tr.AddDescription("(");
                        tr.AddDescription("\'" + memberAccess.Member.GetARFieldAttribute().DatabaseName + "\'");
                        tr.AddDescription(" LIKE \"%");
                        tr.AddDescription((string)constant.Value);
                        tr.AddDescription("%\")");
                        break;
                    case "StartsWith":
                        tr.AddDescription("(");
                        tr.AddDescription("\'" + memberAccess.Member.GetARFieldAttribute().DatabaseName + "\'");
                        tr.AddDescription(" LIKE \"");
                        tr.AddDescription((string)constant.Value);
                        tr.AddDescription("%\")");
                        break;
                    case "EndsWith":
                        tr.AddDescription("(");
                        tr.AddDescription("\'" + memberAccess.Member.GetARFieldAttribute().DatabaseName + "\'");
                        tr.AddDescription(" LIKE \"%");
                        tr.AddDescription((string)constant.Value);
                        tr.AddDescription("\")");
                        break;
                    default:
                        throw new NotSupportedException(string.Format("The method '{0}' is not supported", m.Method.Name));
                }
            }
            else if (m.Method.DeclaringType == typeof(Enumerable))
            {
                switch (m.Method.Name)
                {
                    case "Count":
                        this.Visit(m.Arguments[0]);
                        if (m.Arguments.Count == 2)
                            throw new NotSupportedException("Count clause not support parameter,use where clause instead.");
                        tr.StatisticStatusCode = 1;
                        break;
                    case "Average":
                        this.Visit(m.Arguments[0]);
                        if (m.Arguments.Count != 2)
                            throw new NotSupportedException("Statistics clause must supply the MemberExpression");
                        LambdaExpression Average = (LambdaExpression)StripQuotes(m.Arguments[1]);
                        tr.CurrentOpeation = TranslateResult.Operation.statisticnocount;
                        if (!(Average.Body is MemberExpression))
                            throw new NotSupportedException("Statistics clause only support the MemberExpression");
                        tr.StatisticStatusCode = 3;
                        this.Visit(Average.Body);
                        break;
                    case "Max":
                        this.Visit(m.Arguments[0]);
                        if (m.Arguments.Count != 2)
                            throw new NotSupportedException("Statistics clause must supply the MemberExpression");
                        LambdaExpression Max = (LambdaExpression)StripQuotes(m.Arguments[1]);
                        tr.CurrentOpeation = TranslateResult.Operation.statisticnocount;
                        if (!(Max.Body is MemberExpression))
                            throw new NotSupportedException("Statistics clause only support the MemberExpression");
                        tr.StatisticStatusCode = 5;
                        this.Visit(Max.Body);
                        break;
                    case "Min":
                        this.Visit(m.Arguments[0]);
                        if (m.Arguments.Count != 2)
                            throw new NotSupportedException("Statistics clause must supply the MemberExpression");
                        LambdaExpression Min = (LambdaExpression)StripQuotes(m.Arguments[1]);
                        tr.CurrentOpeation = TranslateResult.Operation.statisticnocount;
                        if (!(Min.Body is MemberExpression))
                            throw new NotSupportedException("Statistics clause only support the MemberExpression");
                        tr.StatisticStatusCode = 4;
                        this.Visit(Min.Body);
                        break;
                    case "Sum":
                        this.Visit(m.Arguments[0]);
                        if (m.Arguments.Count != 2)
                            throw new NotSupportedException("Statistics clause must supply the MemberExpression");
                        LambdaExpression Sum = (LambdaExpression)StripQuotes(m.Arguments[1]);
                        tr.CurrentOpeation = TranslateResult.Operation.statisticnocount;
                        if (!(Sum.Body is MemberExpression))
                            throw new NotSupportedException("Statistics clause only support the MemberExpression");
                        tr.StatisticStatusCode = 2;
                        this.Visit(Sum.Body);
                        break;
                    default:
                        throw new NotSupportedException(string.Format("The method '{0}' is not supported", m.Method.Name));
                }
            }
            else
            {
                throw new NotSupportedException(string.Format("The method '{0}' is not supported", m.Method.Name));
            }
            return m;
        }

        protected override Expression VisitUnary(UnaryExpression u)
        {
            switch (u.NodeType)
            {
                case ExpressionType.Not:
                    {
                        if (tr.CurrentOpeation == TranslateResult.Operation.where)
                        {
                            tr.AddDescription(" NOT( ");
                            this.Visit(u.Operand);
                            tr.AddDescription(")");
                        }
                        break;
                    }
                case ExpressionType.Convert:
                    return this.Visit(u.Operand);
                default:
                    throw new NotSupportedException(string.Format("The unary operator '{0}' is not supported", u.NodeType));
            }
            return u;
        }

        protected override Expression VisitBinary(BinaryExpression b)
        {
            tr.AddDescription("(");
            Expression eTmp = this.Visit(b.Left);
            switch (b.NodeType)
            {
                case ExpressionType.AndAlso:
                case ExpressionType.And:
                    tr.AddDescription(" AND ");
                    break;
                case ExpressionType.Or:
                case ExpressionType.OrElse:
                    tr.AddDescription(" OR ");
                    break;
                case ExpressionType.Equal:
                    tr.AddDescription(" = ");
                    break;
                case ExpressionType.NotEqual:
                    tr.AddDescription(" != ");
                    break;
                case ExpressionType.LessThan:
                    tr.AddDescription(" < ");
                    break;
                case ExpressionType.LessThanOrEqual:
                    tr.AddDescription(" <= ");
                    break;
                case ExpressionType.GreaterThan:
                    tr.AddDescription(" > ");
                    break;
                case ExpressionType.GreaterThanOrEqual:
                    tr.AddDescription(" >= ");
                    break;
                default:
                    throw new NotSupportedException(string.Format("The binary operator '{0}' is not supported", b.NodeType));
            }
            if (b.Right.NodeType == ExpressionType.Constant && eTmp.NodeType == ExpressionType.MemberAccess)
            {
                VisitConstant((ConstantExpression)b.Right, (MemberExpression)eTmp);
            }
            else
                this.Visit(b.Right);
            tr.AddDescription(")");
            return b;
        }

        private Expression VisitConstant(ConstantExpression c, MemberExpression me)
        {
            if (me.Type.IsEnum)
                return Visit(Expression.Constant(Enum.GetName(me.Type, c.Value)));
            else
                return Visit(c);
        }

        protected override Expression VisitConstant(ConstantExpression c)
        {
            IQueryable q = c.Value as IQueryable;
            if (q != null)
            {
                // assume constant nodes w/ IQueryables are table references
                //store Generic Type
                //tr.ModelType = q.ElementType;
            }
            else if (c.Value == null)
            {
                tr.AddDescription("$NULL$");
            }
            else
            {
                switch (Type.GetTypeCode(c.Value.GetType()))
                {
                    case TypeCode.String:
                        tr.AddDescription("\"");
                        tr.AddDescription((string)c.Value);
                        tr.AddDescription("\"");
                        break;
                    case TypeCode.Int32:
                    case TypeCode.Int16:
                    case TypeCode.Int64:
                    case TypeCode.Decimal:
                    case TypeCode.Double:
                        tr.AddDescription(c.Value.ToString());
                        break;
                    case TypeCode.DateTime://value should support datetime
                        tr.AddDescription("\"");
                        tr.AddDescription(c.Value.ToString());
                        tr.AddDescription("\"");
                        break;
                    default:
                        throw new NotSupportedException(string.Format("The constant for '{0}' is not supported", c.Value));
                }
            }
            return c;
        }

        protected override Expression VisitMember(MemberExpression node)
        {

            switch (tr.CurrentOpeation)
            {
                case TranslateResult.Operation.where:
                case TranslateResult.Operation.statisticnocount:
                    if (node.Member == null) return node;

                    string arFieldName = node.Member.GetARFieldAttribute().DatabaseName;

                    tr.AddDescription("\'" + arFieldName + "\'");

                    break;
                case TranslateResult.Operation.select:
                    if (tr.GroupIds.Count == 0)
                    {
                        tr.AddDescription(node.Member.GetARFieldAttribute().DatabaseID.ToString());
                    }
                    break;
                case TranslateResult.Operation.groupby:
                case TranslateResult.Operation.orderby:
                    tr.AddDescription(node.Member.GetARFieldAttribute().DatabaseID.ToString());
                    break;
                case TranslateResult.Operation.orderbydescending:
                    tr.AddDescription((-node.Member.GetARFieldAttribute().DatabaseID).ToString());
                    break;
                default:
                    break;
            }
            return node;
        }
    }
}
